/**
 * Copyright 2007-2010 Arthur Blake
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
 * License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
 * language governing permissions and limitations under the License.
 */
package cn.wen.log4jdbc;

import java.io.FileReader;
import java.io.LineNumberReader;
import java.io.PrintStream;
import java.util.List;
import java.util.LinkedList;
import java.util.Arrays;

/**
 * Post processes an existing sqltiming log, and creates a profiling report from it. Name of log file is passed in on the
 * command line as the only argument.
 * 
 * Assumptions:
 * 
 * 1. Each sql statement in the log is separated by a blank line. 2. Each sql statement is terminated with the timing string
 * "{executed in N msec}" where N is the number of milliseconds that the sql executed in.
 * 
 */
public class PostLogProfilerProcessor {

    // todo: needs to be able to gracefully handle sql exceptions in log output

    /**
     * Post Process log4jdbc sqltiming log data.
     * 
     * @param args command line arguments. Expects one argument, the name of the file to post process.
     * @throws Exception if something goes wrong during processing.
     */
    public static void main(String[] args) throws Exception {
	if (args.length < 1) {
	    System.out.println("usage: java PostLogProfilerProcessor <log-file>");
	    System.exit(1);
	}
	new PostLogProfilerProcessor(args[0], System.out);
    }

    /**
     * Total number of sql statements processed.
     */
    private long totalSql = 0L;

    /**
     * Number of lines processed.
     */
    private long lineNo = 0L;

    /**
     * Total number of milliseconds that all processed sql took to run.
     */
    private long totalMsec = 0L;

    /**
     * Milliseconds of the worst single offending sql statement.
     */
    private long maxMsec = 0L;

    /**
     * Total combined milliseconds of all flagged sql statements.
     */
    private long flaggedSqlTotalMsec = 0L;

    /**
     * Threshold at which sql is deemed to be running slow enough to be flagged.
     */
    private long threshold = 100L;

    /**
     * How many top offender sql statements to display in final report
     */
    private long topOffenderCount = 1000L;

    /**
     * Collection of all sql that took longer than "threshold" msec to run.
     */
    private List flaggedSql = new LinkedList();

    /**
     * Process given filename, and produce sql profiling report to given PrintStream.
     * 
     * @param filename sqltiming log to process.
     * @param out PrintStream to write profiling report to.
     * @throws Exception if reading error occurs.
     */
    public PostLogProfilerProcessor(String filename, PrintStream out) throws Exception {
	FileReader f = new FileReader(filename);
	LineNumberReader l = new LineNumberReader(f);

	String line;
	boolean blankLine;

	StringBuffer sql = new StringBuffer();

	do {
	    line = l.readLine();

	    if (line != null) {
		blankLine = line.length() == 0;
		lineNo++;
		/*
		 * if (lineNo%100000L==0L) { out.println("" + lineNo + " lines..."); }
		 */
		if (blankLine) {
		    processSql(sql);
		    sql = new StringBuffer();
		}
		else {
		    sql.append(line);
		}

	    }
	} while (line != null);

	out.println("processed " + lineNo + " lines.");

	f.close();

	// display report to stdout

	out.println("Number of sql statements:  " + totalSql);
	out.println("Total number of msec    :  " + totalMsec);
	if (totalMsec > 0) {
	    out.println("Average msec/statement  :  " + totalSql / totalMsec);
	}

	int flaggedSqlStmts = flaggedSql.size();

	if (flaggedSqlStmts > 0) {
	    out.println("Sql statements that took more than " + threshold + " msec were flagged.");
	    out.println("Flagged sql statements              :  " + flaggedSqlStmts);
	    out.println("Flagged sql Total number of msec    :  " + flaggedSqlTotalMsec);
	    out.println("Flagged sql Average msec/statement  :  " + flaggedSqlTotalMsec / flaggedSqlStmts);

	    out.println("sorting...");

	    Object[] flaggedSqlArray = flaggedSql.toArray();
	    Arrays.sort(flaggedSqlArray);

	    int execTimeSize = ("" + maxMsec).length();

	    if (topOffenderCount > flaggedSqlArray.length) {
		topOffenderCount = flaggedSqlArray.length;
	    }

	    out.println("top " + topOffenderCount + " offender" + (topOffenderCount == 1 ? "" : "s") + ":");

	    ProfiledSql p;

	    for (int i = 0; i < topOffenderCount; i++) {
		p = (ProfiledSql) flaggedSqlArray[i];
		out.println(Utilities.rightJustify(execTimeSize, "" + p.getExecTime()) + " " + p.getSql());
	    }
	}
    }

    private void processSql(StringBuffer sql) {
	if (sql.length() > 0) {
	    totalSql++;
	    String sqlStr = sql.toString();
	    if (sqlStr.endsWith("msec}")) {
		int executedIn = sqlStr.indexOf("{executed in ");
		if (executedIn == -1) {
		    System.err.println("WARNING:  sql w/o timing info found at line " + lineNo);
		    return;
		}

		// todo: proper error handling for parse
		String msecStr = sqlStr.substring(executedIn + 13, sqlStr.length() - 6);
		long msec = Long.parseLong(msecStr);
		totalMsec += msec;
		if (msec > maxMsec) {
		    maxMsec = msec;
		}

		if (msec > threshold) {
		    flagSql(msec, sqlStr);
		    flaggedSqlTotalMsec += msec;
		}
	    }
	    else {
		System.err.println("WARNING:  sql w/o timing info found at line " + lineNo);
	    }
	}
    }

    private void flagSql(long msec, String sql) {
	flaggedSql.add(new ProfiledSql(msec, sql));
    }

    private class ProfiledSql implements Comparable {
	private Long execTime;
	private String sql;

	public ProfiledSql(long msec, String sql) {
	    this.execTime = new Long(msec);
	    this.sql = sql;
	}

	/**
	 * Compares this object with the specified object for order. Returns a negative integer, zero, or a positive integer as
	 * this object is less than, equal to, or greater than the specified object.
	 * <p>
	 * 
	 * In this case the comparison is used to sort flagged sql in descending order.
	 * 
	 * @param o ProfiledSql Object to compare to this ProfiledSql. Must not be null.
	 */
	public int compareTo(Object o) {
	    return ((ProfiledSql) o).execTime.compareTo(execTime);
	}

	public Long getExecTime() {
	    return execTime;
	}

	public String getSql() {
	    return sql;
	}

	public String toString() {
	    return this.execTime + " msec:  " + this.sql;
	}
    }
}
